using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Dapper {
    /// <summary>
    /// Dynamic query.
    /// Code reference:
    /// 1. http://www.bradoncode.com/blog/2012/12/creating-data-repository-using-dapper.html
    /// 2. https://stackoverflow.com/questions/33484295/dynamic-queries-in-dapper
    /// </summary>
    internal static class DynamicQuery {
        public static string GetUpdateQuery(string tableName,dynamic item) {
            PropertyInfo[] props = item.GetType().GetProperties();
            string[] columns = props.Select(p => p.Name).ToArray();

            List<string> parameters = columns.Select(name => name + "=@" + name).ToList();

            return string.Format("UPDATE {0} SET {1} WHERE ID=@ID",tableName,string.Join(",",parameters));
        }

        public static string GetInsertQuery(string tableName,dynamic item) {
            PropertyInfo[] props = item.GetType().GetProperties();
            string[] columns = props.Select(p => p.Name).Where(s => s != "ID").ToArray();

            return string.Format("INSERT INTO {0} ({1}) OUTPUT inserted.ID VALUES (@{2})",
                                 tableName,
                                 string.Join(",",columns),
                                 string.Join(",@",columns));
        }

        public static QueryResult GetDynamicQuery<T>(string tableName,Expression<Func<T,bool>> expression) {
            List<QueryParameter> queryProperties = new List<QueryParameter>();
            BinaryExpression body = (BinaryExpression)expression.Body;
            IDictionary<string,object> expando = new ExpandoObject();
            StringBuilder builder = new StringBuilder();

            // walk the tree and build up a list of query parameter objects
            // from the left and right branches of the expression tree
            WalkTree(body,ExpressionType.Default,ref queryProperties);

            // convert the query parms into a SQL string and dynamic property object
            builder.Append("SELECT * FROM ");
            builder.Append(tableName);
            builder.Append(" WHERE ");
            for(int i = 0;i < queryProperties.Count();i++) {
                QueryParameter item = queryProperties[i];
                if(!string.IsNullOrEmpty(item.LinkingOperator) && i > 0) {
                    builder.Append(string.Format("{0} [{1}] {2} @{1} ",item.LinkingOperator,item.PropertyName,item.QueryOperator));
                } else {
                    builder.Append(string.Format("[{0}] {1} @{0} ",item.PropertyName,item.QueryOperator));
                }
                expando[item.PropertyName] = item.PropertyValue;
            }
            return new QueryResult(builder.ToString().TrimEnd(),expando);
        }

        private static void WalkTree(BinaryExpression body,ExpressionType linkingType,ref List<QueryParameter> queryProperties) {
            if(body.NodeType != ExpressionType.AndAlso && body.NodeType != ExpressionType.OrElse) {
                string propertyName = GetPropertyName(body);
                object propertyValue = GetPropertyValue(body.Right);
                string opr = GetOperator(body.NodeType);
                string link = GetOperator(linkingType);
                queryProperties.Add(new QueryParameter(link,propertyName,propertyValue,opr));
            } else {
                try {
                    if(body is BinaryExpression) {
                        WalkTree((BinaryExpression)body.Left,body.NodeType,ref queryProperties);
                        WalkTree((BinaryExpression)body.Right,body.NodeType,ref queryProperties);
                    }
                } catch(Exception ex) {
                    Logger.LogError("WalkTree:",ex);
                    return;
                }
            }
        }
        private static object GetPropertyValue(Expression source) {
            ConstantExpression constantExpression = source as ConstantExpression;
            if(constantExpression != null) {
                return constantExpression.Value;
            }

            Expression<Func<object>> evalExpr = Expression.Lambda<Func<object>>(Expression.Convert(source,typeof(object)));
            Func<object> evalFunc = evalExpr.Compile();
            object value = evalFunc();
            return value;
        }
        private static string GetPropertyName(BinaryExpression body) {
            string propertyName = body.Left.ToString().Split(new char[] { '.' })[1];
            if(body.Left.NodeType == ExpressionType.Convert) {
                // hack to remove the trailing ) when convering.
                propertyName = propertyName.Replace(")",string.Empty);
            }
            return propertyName;
        }
        private static string GetOperator(ExpressionType type) {
            switch(type) {
                case ExpressionType.Equal:
                    return "=";
                case ExpressionType.NotEqual:
                    return "!=";
                case ExpressionType.LessThan:
                    return "<";
                case ExpressionType.GreaterThan:
                    return ">";
                case ExpressionType.AndAlso:
                case ExpressionType.And:
                    return "AND";
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    return "OR";
                case ExpressionType.Default:
                    return string.Empty;
                case ExpressionType.GreaterThanOrEqual:
                    return ">=";
                case ExpressionType.LessThanOrEqual:
                    return "<=";
                default:
                    throw new NotImplementedException();
            }
        }
    }
}